﻿#region Namespaces

using System;
using System.Collections.Generic;
using System.IO;
using System.Linq;
using System.Text;
using System.Xml;
using System.Xml.XPath;

using Vibstudio.X4NET.Csv;
using Vibstudio.X4NET.Xml.Generation;
using Vibstudio.X4NET.Xml.Linq;
using Vibstudio.X4NET.Xml.Navigation;
using Vibstudio.X4NET.Xml.Schema;
using Vibstudio.X4NET.Xml.XPath;

#endregion

namespace Vibstudio.X4NET.Xml.Conversion
{
    public class XmlConverter : IXmlConversion
    {
        #region CSV -> XML

        #region Dynamic configuration algorithms

        public OperationResult<XmlDocument> CsvToXml(string csvPath, string xsdPath, bool validate = true, bool omitXmlDeclaration = false)
        {
            return CsvToXml(File.ReadAllBytes(csvPath), xsdPath, validate, omitXmlDeclaration);
        }

        public ConversionResult<string, XmlDocument> CsvToXml(string csvHeaderRow, string csvDataRow, string xsdPath, bool validate = true, bool omitXmlDeclaration = false)
        {
            string[] csvHeader = csvHeaderRow.Split(Defaults.CsvSeparator);
            string[] csvData = csvDataRow.Split(Defaults.CsvSeparator);

            OperationResult<XmlDocument> operationResult = ConvertCsvToXml(xsdPath, csvHeader, csvData, validate, omitXmlDeclaration);

            return operationResult.IsPerformed
                       ? ConversionResult<string, XmlDocument>.CreateSuccess(csvDataRow, operationResult.Output)
                       : ConversionResult<string, XmlDocument>.CreateFailure(csvDataRow, operationResult.Message);
        }

        public IEnumerable<ConversionResult<string, XmlDocument>> CsvToXml(string[] csvRows, string xsdPath, bool validate = true, bool omitXmlDeclaration = false)
        {
            IList<ConversionResult<string, XmlDocument>> result = new List<ConversionResult<string, XmlDocument>>();

            if (csvRows[0].EndsWith("\r"))
            {
                csvRows[0] = csvRows[0].Substring(0, csvRows[0].Length - 1);
            }

            string[] csvHeader = csvRows[0].Split(Defaults.CsvSeparator);

            // Si parte dalla posizione 1 per escludere l'intestazione
            for (int i = 1; i < csvRows.Length; i++)
            {
                string[] csvData = csvRows[i].Split(Defaults.CsvSeparator);

                OperationResult<XmlDocument> conversionResult = ConvertCsvToXml(xsdPath, csvHeader, csvData, validate, omitXmlDeclaration);
                result.Add(conversionResult.IsPerformed
                               ? ConversionResult<string, XmlDocument>.CreateSuccess(csvRows[i], conversionResult.Output)
                               : ConversionResult<string, XmlDocument>.CreateFailure(csvRows[i], conversionResult.Message));
            }

            return result;
        }

        public OperationResult<XmlDocument> CsvToXml(byte[] csvFile, string xsdPath, bool validate = true, bool omitXmlDeclaration = false)
        {
            string[] csvRows = csvFile.ToRows();
            string[] csvHeader = csvRows[0].Split(Defaults.CsvSeparator);
            string[] csvData = csvRows[1].Split(Defaults.CsvSeparator);

            return ConvertCsvToXml(xsdPath, csvHeader, csvData, validate, omitXmlDeclaration);
        }

        #endregion

        #region Parameters configuration algorithms

        public OperationResult<XmlDocument> CsvToXml(string csvPath, CsvConversionConfig config, bool validate = true, bool omitXmlDeclaration = false)
        {
            return CsvToXml(File.ReadAllBytes(csvPath), config, validate, omitXmlDeclaration);
        }

        public IEnumerable<ConversionResult<string, XmlDocument>> CsvToXml(string[] csvRows, CsvConversionConfig config, bool validate = true, bool omitXmlDeclaration = false)
        {
            if (config == null)
            {
                throw new ArgumentNullException("config");
            }

            IList<ConversionResult<string, XmlDocument>> result = new List<ConversionResult<string, XmlDocument>>();

            if (!string.IsNullOrEmpty(config.CsvHeaderRow))
            {
                string[] csvHeader = config.CsvHeaderRow.Split(Defaults.CsvSeparator);

                // Si parte dalla posizione 1 per escludere l'intestazione
                for (int i = 1; i < csvRows.Length; i++)
                {
                    string[] csvData = csvRows[i].Split(Defaults.CsvSeparator);

                    OperationResult<XmlDocument> conversionResult = ConvertCsvToXml(config.XsdPath, csvHeader, csvData, validate, omitXmlDeclaration);
                    result.Add(conversionResult.IsPerformed
                                   ? ConversionResult<string, XmlDocument>.CreateSuccess(csvRows[i], conversionResult.Output)
                                   : ConversionResult<string, XmlDocument>.CreateFailure(csvRows[i], conversionResult.Message));
                }

                return result;
            }

            if (string.IsNullOrEmpty(config.XsdPath))
            {
                throw new ArgumentException("config.XsdPath");
            }

            return CsvToXml(csvRows, config.XsdPath, validate, omitXmlDeclaration);
        }

        public OperationResult<XmlDocument> CsvToXml(byte[] csvFile, CsvConversionConfig config, bool validate = true, bool omitXmlDeclaration = false)
        {
            if (config == null)
            {
                throw new ArgumentNullException("config");
            }

            string[] csvRows = csvFile.ToRows();
            string[] csvData = csvRows[1].Split(Defaults.CsvSeparator);

            if (!string.IsNullOrEmpty(config.CsvHeaderRow))
            {
                return ConvertCsvToXml(csvData, config, validate, omitXmlDeclaration);
            }

            if (string.IsNullOrEmpty(config.XsdPath))
            {
                throw new ArgumentException("config.XsdPath");
            }

            string[] csvHeader = csvRows[0].Split(Defaults.CsvSeparator);
            return ConvertCsvToXml(config.XsdPath, csvHeader, csvData, validate, omitXmlDeclaration);
        }

        #endregion

        #endregion

        #region XML -> CSV

        #region Dynamic configuration algorithms

        public ConversionResult<XmlDocument, CsvDocument> XmlToCsv(string xmlPath, string xsdPath)
        {
            return XmlToCsv(File.ReadAllBytes(xmlPath), xsdPath);
        }

        public ConversionResult<XmlDocument, CsvDocument> XmlToCsv(byte[] xmlFile, string xsdPath)
        {
            if (xmlFile == null)
            {
                throw new ArgumentNullException("xmlFile");
            }

            if (string.IsNullOrEmpty(xsdPath))
            {
                throw new ArgumentNullException("xsdPath");
            }

            XmlDocument input = new XmlDocument();

            try
            {
                using (Stream stream = new MemoryStream(xmlFile))
                {
                    input.Load(stream);

                    CsvDocument output = ComposeOutputDynamically(input, xsdPath);

                    return ConversionResult<XmlDocument, CsvDocument>.CreateSuccess(input, output);
                }
            }
            catch (Exception exception)
            {
                return ConversionResult<XmlDocument, CsvDocument>.CreateFailure(input, exception.Message);
            }
        }

        public ConversionResult<XmlDocument, CsvDocument> XmlToCsv(XmlDocument document, string xsdPath)
        {
            if (document == null)
            {
                throw new ArgumentNullException("document");
            }

            if (string.IsNullOrEmpty(xsdPath))
            {
                throw new ArgumentNullException("xsdPath");
            }

            XmlDocument input = document;

            try
            {
                CsvDocument output = ComposeOutputDynamically(input, xsdPath);

                return ConversionResult<XmlDocument, CsvDocument>.CreateSuccess(input, output);
            }
            catch (Exception exception)
            {
                return ConversionResult<XmlDocument, CsvDocument>.CreateFailure(input, exception.Message);
            }
        }

        #endregion

        #region Parameters configuration algorithms

        public ConversionResult<XmlDocument, CsvDocument> XmlToCsv(string xmlPath, CsvConversionConfig config)
        {
            return XmlToCsv(File.ReadAllBytes(xmlPath), config);
        }

        public ConversionResult<XmlDocument, CsvDocument> XmlToCsv(byte[] xmlFile, CsvConversionConfig config)
        {
            if (xmlFile == null)
            {
                throw new ArgumentNullException("xmlFile");
            }

            if (config == null)
            {
                throw new ArgumentNullException("config");
            }

            // Se l'intestazione CSV è vuota o non esistono impostazioni di conversione
            // viene eseguita la conversione nella modalità "dinamica".
            if (string.IsNullOrEmpty(config.CsvHeaderRow) || config.Settings == null || config.Settings.Count == 0)
            {
                return XmlToCsv(xmlFile, config.XsdPath);
            }

            XmlDocument input = new XmlDocument();

            try
            {
                using (Stream stream = new MemoryStream(xmlFile))
                {
                    input.Load(stream);

                    CsvDocument output = ComposeOutputParametrically(input, config);

                    return ConversionResult<XmlDocument, CsvDocument>.CreateSuccess(input, output);
                }
            }
            catch (Exception exception)
            {
                return ConversionResult<XmlDocument, CsvDocument>.CreateFailure(input, exception.Message);
            }
        }

        public ConversionResult<XmlDocument, CsvDocument> XmlToCsv(XmlDocument document, CsvConversionConfig config)
        {
            if (document == null)
            {
                throw new ArgumentNullException("document");
            }

            if (config == null)
            {
                throw new ArgumentNullException("config");
            }

            if (string.IsNullOrEmpty(config.XsdPath))
            {
                throw new ArgumentException("config.XsdPath");
            }

            if (config.Settings == null || config.Settings.Count == 0)
            {
                return XmlToCsv(document, config.XsdPath);
            }

            XmlDocument input = document;

            try
            {
                CsvDocument output = ComposeOutputParametrically(input, config);

                return ConversionResult<XmlDocument, CsvDocument>.CreateSuccess(input, output);
            }
            catch (Exception exception)
            {
                return ConversionResult<XmlDocument, CsvDocument>.CreateFailure(input, exception.Message);
            }
        }

        #endregion

        #endregion

        #region Helpers

        private OperationResult<XmlDocument> ConvertCsvToXml(string[] csvDataRow, CsvConversionConfig config, bool validate = true, bool omitXmlDeclaration = false)
        {
            try
            {
                XmlDocument document = config.XmlTemplate;

                FillDocumentFromCsvData(csvDataRow, config.Settings.Select(x => x.XPath), document);

                OperationResult<XmlDocument> result = OperationResult<XmlDocument>.Default(document);

                if (validate)
                {
                    OperationResult validationResult = ValidateXml(document.OuterXml, config.XsdPath);
                    if (!validationResult.IsPerformed)
                    {
                        result.Output = FormatXml(document.OuterXml, omitXmlDeclaration).Output;
                        result.IsPerformed = false;
                        result.Message = validationResult.Message;
                        return result;
                    }
                }

                return FormatXml(document.OuterXml, omitXmlDeclaration);
            }
            catch (Exception exception)
            {
                return OperationResult<XmlDocument>.CreateFailure(exception.Message);
            }
        }

        private OperationResult<XmlDocument> ConvertCsvToXml(string xsdPath, string[] csvHeader, string[] csvData, bool validate = true, bool omitXmlDeclaration = false)
        {
            try
            {
                string template = GetXmlTemplate(xsdPath);
                IList<string> tempXPaths = new XmlNavigator().GetKeyValuePairs(XDocument.Parse(template),
                                                                               string.Empty,
                                                                               string.Empty,
                                                                               @"/",
                                                                               false,
                                                                               true,
                                                                               SearchCriterion.None,
                                                                               string.Empty).XPaths;

                IEnumerable<XPathInfo> xpaths = SynchronizeXPathsToCsv(tempXPaths, csvHeader);

                XmlDocument document = new XmlDocument();
                document.LoadXml(template);

                FillDocumentFromCsvData(csvData, xpaths, document);

                OperationResult<XmlDocument> result = FormatXml(document.OuterXml, omitXmlDeclaration);

                if (validate)
                {
                    OperationResult validationResult = ValidateXml(result.Output.OuterXml, xsdPath);
                    if (!validationResult.IsPerformed)
                    {
                        result.IsPerformed = false;
                        result.Message = validationResult.Message;
                        return result;
                    }
                }

                return result;
            }
            catch (Exception exception)
            {
                return OperationResult<XmlDocument>.CreateFailure(exception.Message);
            }
        }

        private CsvDocument ComposeOutputDynamically(XmlDocument input, string xsdPath)
        {
            XmlDocument documentHeader = new XmlDocument();
            documentHeader.LoadXml(GetXmlTemplate(xsdPath));

            IList<string> xpaths = new XmlNavigator().GetKeyValuePairs(XDocument.Parse(documentHeader.InnerXml),
                                                                       string.Empty,
                                                                       string.Empty,
                                                                       @"/",
                                                                       false,
                                                                       true,
                                                                       SearchCriterion.None,
                                                                       string.Empty).XPaths;

            XPathNavigator headerNavigator = documentHeader.CreateNavigator();
            XPathNavigator dataNavigator = input.CreateNavigator();

            StringBuilder csvHeader = new StringBuilder();
            StringBuilder csvData = new StringBuilder();

            foreach (string xpath in xpaths)
            {
                XPathNavigator headerNode = headerNavigator.SelectSingleNode(xpath);
                if (headerNode != null)
                {
                    csvHeader.AppendFormat("{0}{1}", headerNode.Name, Defaults.CsvSeparator);

                    XPathNavigator dataNode = dataNavigator.SelectSingleNode(xpath);
                    //csvData.AppendFormat("{0}{1}", dataNode != null ? dataNode.EscapeXml() : string.Empty, CsvConfig.DefaultSeparator);
                    csvData.AppendFormat("{0}{1}", dataNode != null ? dataNode.Value.EscapeCsv() : string.Empty, Defaults.CsvSeparator);
                }
            }

            csvHeader.Length--;
            csvData.Length--;

            string csv = string.Format("{0}{2}{1}{2}", csvHeader, csvData, Environment.NewLine);

            CsvDocument output = new CsvDocument();
            output.LoadCsv(csv);

            return output;
        }

        private static CsvDocument ComposeOutputParametrically(XmlDocument input, CsvConversionConfig config)
        {
            XPathNavigator dataNavigator = input.CreateNavigator();

            StringBuilder csvDataRowBuilder = new StringBuilder();
            foreach (CsvConversionSetting setting in config.Settings)
            {
                XPathNavigator dataNode = dataNavigator.SelectSingleNode(setting.XPath.Value);
                //csvDataRowBuilder.AppendFormat("{0}{1}", dataNode != null ? dataNode.EscapeXml() : string.Empty, CsvConfig.DefaultSeparator);
                csvDataRowBuilder.AppendFormat("{0}{1}", dataNode != null ? dataNode.Value.EscapeCsv() : string.Empty, Defaults.CsvSeparator);
            }
            csvDataRowBuilder.Length--;

            string csv = string.Format("{0}{2}{1}{2}", config.CsvHeaderRow, csvDataRowBuilder, Environment.NewLine);

            CsvDocument output = new CsvDocument();
            output.LoadCsv(csv);

            return output;
        }

        private void FillDocumentFromCsvData(string[] csvDataRow, IEnumerable<XPathInfo> xpaths, XmlDocument document)
        {
            foreach (XPathInfo xpath in xpaths)
            {
                XmlNode node = document.SelectSingleNode(xpath.Value);
                if (node != null)
                {
                    if (!string.IsNullOrEmpty(node.Name))
                    {
                        if (xpath.Index > -1)
                        {
                            string value = csvDataRow[xpath.Index];
                            if (node.SelectSingleNode(xpath.Value) != null && !string.IsNullOrEmpty(value))
                            {
                                node.LastChild.Value = value;
                            }
                            else
                            {
                                node.RemoveAll();
                            }
                        }
                        else
                        {
                            node.RemoveAll();
                        }
                    }
                    else
                    {
                        node.RemoveAll();
                    }
                }
            }
        }

        private IEnumerable<XPathInfo> SynchronizeXPathsToCsv(IEnumerable<string> xpaths, string[] csvHeader)
        {
            IList<XPathInfo> list = new List<XPathInfo>();

            int i = 0;
            foreach (string xpath in xpaths)
            {
                XPathInfo candidateXPath = XPathInfo.Create(xpath);

                string tagName = candidateXPath.TagName;

                for (int j = i; j < csvHeader.Length; j++)
                {
                    if (string.Equals(tagName, csvHeader[j], StringComparison.InvariantCultureIgnoreCase))
                    {
                        candidateXPath.Index = i;
                        i++;
                        break;
                    }
                }

                list.Add(candidateXPath);
            }

            return list;
        }

        private string GetXmlTemplate(string xsdPath, int maxThreshold = 1, int listLength = 1)
        {
            var xmlGen = new XmlSampleGenerator(xsdPath, null)
            {
                MaxThreshold = maxThreshold,
                ListLength = listLength
            };

            StringBuilder builder = new StringBuilder();
            XmlTextWriter writer = new XmlTextWriter(new StringWriter(builder)) { Formatting = Formatting.Indented };
            xmlGen.WriteXml(writer);

            return DeepScanXmlTemplate(builder.ToString());
        }

        private string DeepScanXmlTemplate(string template)
        {
            ASCIIEncoding encoding = new ASCIIEncoding();

            MemoryStream stream = new MemoryStream(encoding.GetBytes(template));

            XmlDocument document = new XmlDocument();
            document.Load(stream);

            XmlNode node = document.DocumentElement;
            if (node != null)
            {
                XmlNodeList nodeList1 = node.ChildNodes;

                for (int i = 0; i < nodeList1.Count; i++)
                {
                    if (nodeList1[i].ChildNodes.Count <= 0)
                    {
                        continue;
                    }

                    if (nodeList1[i].ChildNodes[0].NodeType == XmlNodeType.Text)
                    {
                        nodeList1[i].InnerText = string.Format("{0}{1}", nodeList1[i].Name, 1);
                    }

                    XmlNodeList nodeList2 = nodeList1[i].ChildNodes;

                    for (int j = 0; j < nodeList2.Count; j++)
                    {
                        if (nodeList2[j].ChildNodes.Count <= 0)
                        {
                            continue;
                        }
                        if (nodeList2[j].ChildNodes[0].NodeType == XmlNodeType.Text)
                        {
                            nodeList2[j].InnerText = string.Format("{0}{1}", nodeList2[j].Name, 1);
                        }

                        XmlNodeList nodeList3 = nodeList2[j].ChildNodes;

                        for (int k = 0; k < nodeList3.Count; k++)
                        {
                            if (nodeList3[k].ChildNodes.Count <= 0)
                            {
                                continue;
                            }
                            if (nodeList3[k].ChildNodes[0].NodeType == XmlNodeType.Text)
                            {
                                nodeList3[k].InnerText = string.Format("{0}{1}", nodeList3[k].Name, 1);
                            }
                        }
                    }
                }
            }

            return document.InnerXml;
        }

        private OperationResult<XmlDocument> FormatXml(string xml, bool omitXmlDeclaration = false)
        {
            XmlDocument document = new XmlDocument();
            document.LoadXml(xml);

            return OperationResult<XmlDocument>.CreateSuccess(document.FormatXml(omitXmlDeclaration, true));
        }

        private OperationResult ValidateXml(string xml, string xsdPath)
        {
            return XsdValidator.Validate(xml, xsdPath);
        }

        #endregion
    }
}
